﻿using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Authentication;

namespace SL.M.Net
{
    using Debug;

    /// <summary>
    /// Represent an open server session with a client. This process is created and started automatically by the game server to associate a
    /// client endpoint with a client state object.
    /// </summary>
    /// <remarks>After connecting, the following message types are made available.
    /// <para>A <code>us.sl.mgame.clientAccepted</code> message is sent by the process when the client connection is accepted.</para>
    /// <para>Users can determine when the session has ended by checking the <code>us.sl.m.stop</code> event. The reason for the termination is exposed as a property on the process object.</para>
    /// <see cref="SL.M.Net.GameServer"/>
    /// <see cref="SL.M.GameObject"/>
    /// </remarks>
    public class GameServerSession<T> : Process where T : GameObject
    {
        const string Tag = "session: ";

        internal static readonly MessageType ClientAcceptedType = "us.sl.mgame.clientAccepted";

        ConnectionManager _manager;
        TcpClient _client;
        GameServer<T> _server;
        ServerFrame<T> _frame = new ServerFrame<T>();
        Func<GameObject> _authorizationInitializer;
        Action<GameObject> _authorizationAction;
        SessionFlags _flags;
        int _sessionId;
        bool _sendFrame;
        bool _disposed = false;

        /// <summary>
        /// Return the remote endpoint of the client.
        /// </summary>
        public IPEndPoint RemoteEndpoint
        {
            get
            {
                return (IPEndPoint)_client.Client.RemoteEndPoint;
            }
        }

        /// <summary>
        /// Return the server object for this session.
        /// </summary>
        public T ServerObject
        {
            get
            {
                return _frame.ServerObject;
            }
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="server">The game server associated with this session.</param>
        /// <param name="client">The TCP client to communicate with.</param>
        /// <param name="authorizationInitializer">A functor which initializes the authorization state to be used during the authorization phase for a specified client. The client should send the correct state to authorize itself with the server. If the authorization action fails by throwing an exception, the message will be returned to the client.</param>
        /// <param name="authorizationAction">The authorization action which is used during the authorization phase to determine if the specified game object should be allowed to join the session.</param>
        /// <param name="flags">The session flags, if any.</param>
        /// <param name="sessionId">The session id.</param>
        public GameServerSession(GameServer<T> server, TcpClient client, Func<GameObject> authorizationInitializer, Action<GameObject> authorizationAction, SessionFlags flags, int sessionId)
        {
            if (server == null)
            {
                throw new ArgumentNullException("server");
            }

            if (client == null)
            {
                throw new ArgumentNullException("client");
            }

            if (authorizationInitializer == null)
            {
                throw new ArgumentNullException("authorizationInitializer");
            }

            if (authorizationAction == null)
            {
                throw new ArgumentNullException("authorizationAction");
            }

            _server = server;
            _frame.ServerObject = server.ServerObject;
            _client = client;
            _authorizationInitializer = authorizationInitializer;
            _authorizationAction = authorizationAction;
            _flags = flags;
            _sessionId = sessionId;
        }

        /// <summary>
        /// Session process entry.
        /// </summary>
        protected override void ProcessEntry()
        {
            DebugProvider.Log(DebugProvider.LastHr, Tag + "Starting GameServerSession");

            try
            {
                WrapNetAction(DoServerSession);
            }
            finally
            {
                if (_client != null)
                {
                    _client.Close();
                }
            }
        }

        /// <summary>
        /// Handle a server message.
        /// </summary>
        /// <param name="message">The message to handle</param>
        /// <returns>True if the message was handled.</returns>
        protected override bool HandleMessage(Message message)
        {
            if (message.Sender == _server)
            {
                if (message.TypeEquals(GameServer<T>.ServerFrameType))
                {
                    _sendFrame = true;
                }
            }
            return base.HandleMessage(message);
        }

        /// <summary>
        /// Dispose of this object.
        /// </summary>
        /// <param name="disposing">Whether or not we are being called from a disposer.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", MessageId = "0#")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_frame")]
        protected override void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                _disposed = true;

                if (disposing)
                {
                    if (_manager != null)
                    {
                        _manager.Dispose();
                        _manager = null;
                    }
                }
            }
            base.Dispose(disposing);
        }

        void DoServerSession()
        {
            DebugProvider.Log(DebugProvider.LastHr, Tag + "Client connected from: {0}", RemoteEndpoint);

            // Set up the stream. We have to finish negotiation before we can really do anything else.
            _manager = new ConnectionManager(Dns.GetHostName(), _client, _flags, true);

            GameObject authorizationObject = _authorizationInitializer();
            RecieveFrame(authorizationObject);

            _authorizationAction(authorizationObject);

            if (_server.NumClients > _server.MaxClients)
            {
                throw new SessionAbortedException("Server full.");
            }

            Send(ClientAcceptedType);
            _frame.SessionId = _sessionId;

            while (!_frame.Faulted && Started)
            {
                SendFrame();
            }
        }

        void WrapNetAction(Action netAction)
        {
            Exception thrown = null;
            string faultMessage = null;

            try
            {
                netAction();
            }
            catch (SocketException se)
            {
                faultMessage = "Connection terminated: " + se.Message;
                thrown = se;
            }
            catch (IOException ie)
            {
                faultMessage = "Connection terminated: " + ie.Message;
                thrown = ie;
            }
            catch (SessionAbortedException sa)
            {
                faultMessage = "Session aborted: " + sa.Message;
                thrown = sa;
            }
            catch (BadResultException br)
            {
                faultMessage = "Data corrupt";
                thrown = br;
            }
            catch (AuthenticationException af)
            {
                faultMessage = "SSL negotiation failed";
                thrown = af;
            }
            finally
            {
                if (thrown != null)
                {
                    DebugProvider.Log(DebugProvider.LastHr, Tag + "Client connection failed. Actual exception: " + thrown.ToString());
                    if (!(thrown is SocketException))
                    {
                        _frame.ServerObject = null;
                        _frame.Faulted = true;
                        _frame.Message = faultMessage;
                        try
                        {
                            if (_manager != null)
                            {
                                _manager.SendFrame(_frame);
                            }
                        }
                        catch (SessionAbortedException)
                        {
                        }
                        catch (BadResultException)
                        {
                        }
                        catch (SocketException)
                        {
                        }
                        catch (IOException)
                        {
                        }
                    }
                }
            }
        }

        void SendFrame()
        {
            _sendFrame = false;
            while (!_frame.Faulted && Started)
            {
                NextMessage();
                if (_sendFrame)
                {
                    _manager.SendFrame(_frame);
                    return;
                }
            }
        }

        void RecieveFrame(GameObject targetObject)
        {
            if (!_frame.Faulted && Started)
            {
                _manager.RecieveFrame(targetObject);
            }
        }
    }
}
